package net;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Instances of the NetConnection class represent a peer-to-peer connection. 
 * Internally it manages an input/output stream pair to handle the communication.
 * Normally you don't create a connection instance yourself, but get one from a NetService 
 * object whenever a client connects to that service. After that you can assign a 
 * delegate object to that connection. The delegate object needs to implement the 
 * NetConnectionDelegate interface as those message will be called by the connection
 * to notify the delegate about new incoming messages. To send a message through 
 * the connection you call it's sendMessage() function and pass a NetMessage object.
 * Optionally one can enable pinging. If pinging is enabled, your delegate object will
 * periodically receive notifications about the connection speed.
 */
public class NetConnection {
	private Socket _socket;
	private boolean _hasSocket;
	private InputStream _inputStream;
	private OutputStream _outputStream;
	protected boolean _dataStreams = false;
	public char delimiter = '\n';
	private NetConnectionThread _thread;
	private boolean _running = false;
	public NetConnectionDelegate delegate;
	
	// Variables needed for pinging.
	private boolean _pingEnabled = false;
	private boolean _stopPing = false;
	private long _pingInterval = 1000;
	private Timer _pingTimer;
	private TimerTask _pingTask;
	private final String _pingHeader = "NetConnectionPing ";
	private long _lastPing = 0;
	
	public NetConnection(Socket socket){
		_hasSocket = true;
		try {
			_socket = socket;
			_inputStream = _socket.getInputStream();
			_outputStream = _socket.getOutputStream();
		} catch (Exception e) {
			NetLogger.warning("Can't open streams to client.");
		}
		_thread = new NetConnectionThread(this);
		NetLogger.connection("Connection OK.");
		_thread.start();
	}
	
	public NetConnection(InputStream inputStream, OutputStream outputStream) {
		_hasSocket = false;
		if(inputStream.getClass() == DataInputStream.class && outputStream.getClass() == DataOutputStream.class) 
		{
			_dataStreams = true;
		} else if(inputStream.getClass() == InputStream.class && outputStream.getClass() == OutputStream.class) 
		{
			_dataStreams = false;
		} else {
			NetLogger.log("Wrong stream types in NetConnection constructor.");
			return;
		}
		try {
			_socket = null;
			_inputStream = inputStream;
			_outputStream = outputStream;
		} catch (Exception e) {
			NetLogger.warning("Can't open streams to client.");
		}
		_thread = new NetConnectionThread(this);
		NetLogger.connection("Connection OK.");
		_thread.start();
	}
	
	/*public NetConnection(DataInputStream inputStream, DataOutputStream outputStream) {
		this((InputStream)inputStream, (OutputStream)outputStream);
		_dataStreams = true;
	}*/
	
	public void setPingEnabled(boolean pingEnabled){
		if(_pingEnabled == pingEnabled) return;
		_pingEnabled = pingEnabled;
		if(_pingEnabled){
			_stopPing = false;
			if(_pingTimer == null) _pingTimer = new Timer();
			if(_pingTask == null) _pingTask = new PingTask();
			_pingTimer.schedule(_pingTask, _pingInterval, _pingInterval);
		}
		else {
			_pingTimer.cancel();
		}
	}
	
	public boolean getPingEnabled(){
		return _pingEnabled;
	}
	
	public void setPingInterval(long interval){
		if(_pingInterval == interval) return;
		_pingInterval = interval;
		if(_pingEnabled){
			_pingTimer.cancel();
			_pingTimer.schedule(_pingTask, _pingInterval, _pingInterval);
		}
	}
	
	public long getPingInterval(){
		return _pingInterval;
	}
	
	public long getLastPing(){
		return _lastPing;
	}
	
	protected void finalize() throws Throwable
	{
		_running = false;
		_outputStream.close();
		_inputStream.close();
		if(_hasSocket)
			_socket.close();
		super.finalize();
	} 
	
	public Socket getSocket(){
		return _socket;
	}
	
	public void sendMessage(NetMessage message){
		try {
			String str = message.toString() + delimiter;
			if(_dataStreams) {
				((DataOutputStream)_outputStream).writeBytes(str);
				((DataOutputStream)_outputStream).flush();
			} else {
				_outputStream.write(str.getBytes());
			}
			if(!str.startsWith(_pingHeader))
				NetLogger.message("Sent message: \"" + message.toString() + "\".");
		} catch (IOException e) {
			NetLogger.warning("Couldn't send message \"" + message.toString() + "\".");
		}
	}
	
	class NetConnectionThread extends Thread {
		private NetConnection _netConnection;
		
		public NetConnectionThread(NetConnection netConnection){
			_netConnection = netConnection;
		}
		
		public void run()
		{
			try {
				_running = true;
				int c;
				//String message = "";
				StringBuffer buf = new StringBuffer(1024);
				if(_hasSocket == true && _socket == null || _inputStream == null){
					NetLogger.error("No streams open, closing connection.");
					_running = false;
				}
				while(_running && (_hasSocket ? !_socket.isClosed() : true)
						&& (_netConnection._dataStreams ? 
								(c = ((DataInputStream)_inputStream).readByte()) != 1
								: (c = _inputStream.read()) != -1))
				{
					if((char)c == delimiter)
					{
						String message = buf.toString();
						if(message.startsWith(_pingHeader)){
							String stringDate = buf.substring(_pingHeader.length(), message.length());
							long sentDate = Long.valueOf(stringDate);
							long now = (new Date()).getTime();
							long difference = now - sentDate;
							delegate.connectionReceivedPingBack(_netConnection, difference);
							NetLogger.ping("Ping: " + difference + "ms");
						} else {
							NetMessage netMessage = new NetMessage(message);
							if(delegate != null && _running) {
								NetLogger.message("Got message:  \"" + message + "\".");
								delegate.connectionReceivedMessage(_netConnection, netMessage);
							}
						}
						buf = new StringBuffer(1024); // Reset string for new commands.
					}
					else{
						buf.append((char)c);
						//message = message + String.valueOf((char)c);
						//NetLogger.log("Got: " + message);
					}
					
				}
				_stopPing = true;
				NetLogger.connection("Connection lost.");
			} catch (IOException e) {
				NetLogger.error("Input stream reading error in connection thread.");
				e.printStackTrace();
			}
		}		
	}
	
	class PingTask extends TimerTask {
		public void run() {
			if(!_pingEnabled) return;
			if(_stopPing) return;
			Date now = new Date();
			String nowString = String.valueOf(now.getTime());
			NetMessage pingMessage = new NetMessage(_pingHeader + nowString);
			sendMessage(pingMessage);
		}
	}

	public void close() throws IOException {
		_inputStream.close();
		_outputStream.close();
	}
	
}


